package com.find.memories.photoviewer;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;

import com.find.memories.cache.RecyclingBitmapDrawable;

/**
 * 포토뷰어 진입 시에 처음 보여지는 Zoom기능이 되는 View
 * @author Jang
 *
 */
public class ThumbnailView extends ImageView {

	final static String TAG = "ThumbnailView";
	
	public interface DragReachTheWallListener {
		public void availableSlide(int which);
	}
	
	public static final int MIN_ZOOM = -1;
	public static final int NORMAL = 0;
	public static final int MAX_ZOOM = 1;
	public static final int DOUBLE_TAP = 2;

	final static float ZOOMING_DISTANCE = 10f;

	private static final float MAXZOOM 		 = 5f;
	private static final float DOUBLETAPZOOM_PHASE_1 = 1.7f;
	private static final float DOUBLETAPZOOM_PHASE_2 = 3f;
	
	public enum ThumbnailMode {
		NONE, DRAG, ZOOM;
	}
	
	private boolean mZoomMode = false;
	
	private Matrix mMatrix = new Matrix();
	private Matrix mBackupMatrix = new Matrix();
	private Matrix mBackupMatrix2 = new Matrix();
	
	private PointF mOffsetPoint = new PointF();
	private PointF mMiddlePoint = new PointF();
	private float mDistance = 1f;
	
	private int mViewWidth = 0;
	private int mViewHeight = 0;
	private int mImageWidth = 0;
	private int mImageHeight = 0;
	
	private ThumbnailMode mMode = ThumbnailMode.NONE;

	private boolean mRatioVertical = false;

	private float mOffSetScale = 1f;
	private float mDefalutScale = 0;
	
	private RectF mImageRect = new RectF();
	
	private DragReachTheWallListener mDragReachTheWallListener;
	
	public ThumbnailView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}
	
	public ThumbnailView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}
	
	public ThumbnailView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
	}
	
	void init() {
		this.setScaleType(ImageView.ScaleType.MATRIX);
		setImageMatrix(mMatrix);
		initImgPos();
		initImageRect();
	}
	
	/**
	 * 초기화 시  현재 화면에 보이는 Image의 정확한 Rect값을 생성한다.
	 */
	public void initImageRect() {
		int viewWidth = this.getWidth();
		int viewHeight = this.getHeight();
		if (viewWidth == 0) {
			viewWidth = mViewWidth;
		}
		if (viewHeight == 0) {
			viewHeight = mViewHeight;
		}
		if (mRatioVertical) {
			int temp = (int) (viewWidth - mImageWidth*mOffSetScale);
			mImageRect.set(temp/2, 0, (int) (mImageWidth*mOffSetScale+temp/2), viewHeight);
		} else {
			int temp = (int) (viewHeight - mImageHeight*mOffSetScale);
			mImageRect.set(0, temp/2, viewWidth, (int) (mImageHeight*mOffSetScale+temp/2)); 
		}
	}
	
	@Override
	public void setImageDrawable(Drawable drawable) {
		// TODO Auto-generated method stub
		
		final Drawable previousDrawable = getDrawable();
		
		super.setImageDrawable(drawable);
		
		if (drawable != null) {
			mImageWidth = drawable.getIntrinsicWidth();
			mImageHeight = drawable.getIntrinsicHeight();
			init();
		}
		
        // Notify new Drawable that it is being displayed
        notifyDrawable(drawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		// TODO Auto-generated method stub
		super.onLayout(changed, left, top, right, bottom);
		mViewWidth = right-left;
		mViewHeight = bottom-top;
		init();
	}
	
	@Override
	protected void onAttachedToWindow() {
		// TODO Auto-generated method stub
		super.onAttachedToWindow();
	}
	
	@Override
	protected void onDetachedFromWindow() {
		// TODO Auto-generated method stub
		setImageDrawable(null);
		super.onDetachedFromWindow();
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		super.onDraw(canvas);
	}
	
	/**
	 * 초기화 작업 및 이미지 ratio에 맞춰서 Center정렬한다.
	 */
	public void initImgPos(){
		
		float[] value = new float[9];
		this.mMatrix.getValues(value);
		
		int scaleWidth = (int) (mImageWidth * value[0]);
		int scaleHeight = (int) (mImageHeight * value[4]);
		
		int width = this.getWidth();
		int height = this.getHeight();
		if (width == 0) {
			width = mViewWidth;
		}
		
		if (height == 0) {
			height = mViewHeight;
		}

		float xratio = (float)width / (float)mImageWidth;
		float yratio = (float)height / (float)mImageHeight;
		
		// Math.min fits the image to the shorter axis. (with letterboxes around)
		// Math.max fits the image th the longer axis. (with the other axis cropped)
		if (mImageWidth > width || mImageHeight > height){
			mDefalutScale = Math.min(xratio, yratio);
		} else {
			mDefalutScale = Math.min(xratio, yratio);
		}
		value[0] = mDefalutScale;
		value[4] = mDefalutScale;
		mOffSetScale = mDefalutScale;
		
		mRatioVertical = yratio == mDefalutScale ? true:false;
		
		scaleWidth = (int) (mImageWidth * value[0]);
		scaleHeight = (int) (mImageHeight * value[4]);
		
		// align the image to the top left corner
		//value[2] = 0;
		//value[5] = 0;
		
		// center the image. it will be aligned to the top left corner otherwise.
		value[2] = (float) width / 2 - (float)scaleWidth / 2;
		value[5] = (float) height / 2 - (float)scaleHeight / 2;
		
		mMatrix.setValues(value);
		setBoundOfMatrix();
		setImageMatrix(mMatrix);
		mBackupMatrix2.set(mMatrix);
	}
	
	public void resetImgPos(boolean resizeMin, float forceScale){
		
		float[] value = new float[9];
		this.mMatrix.getValues(value);
		
		int width = this.getWidth();
		int height = this.getHeight();
		if (width == 0) {
			width = mViewWidth;
		}
		
		if (height == 0) {
			height = mViewHeight;
		}
		
		float xratio = (float)width / (float)mImageWidth;
		float yratio = (float)height / (float)mImageHeight;
		
		if (resizeMin) {
			// Math.min fits the image to the shorter axis. (with letterboxes around)
			// Math.max fits the image th the longer axis. (with the other axis cropped)
			if (mImageWidth > width || mImageHeight > height){
				mDefalutScale = Math.min(xratio, yratio);
			} else {
				mDefalutScale = Math.min(xratio, yratio);
			}
		} else {
			mDefalutScale = Math.max(xratio, yratio);
		}

		//mDefalutScale = forceScale;
		value[0] = mDefalutScale;
		value[4] = mDefalutScale;
		
		int scaleWidth = (int) (mImageWidth * value[0]);
		int scaleHeight = (int) (mImageHeight * value[4]);
		
		// align the image to the top left corner
		//value[2] = 0;
		//value[5] = 0;
		
		// center the image. it will be aligned to the top left corner otherwise.
		value[2] = (float) width / 2 - (float)scaleWidth / 2;
		value[5] = (float) height / 2 - (float)scaleHeight / 2;

		mMatrix.setValues(value);
		setImageMatrix(mMatrix);
		mBackupMatrix2.set(mMatrix);
		invalidate();
	}
	
	/**
	 * Interface에서 넘겨받은 Event를 처리한다.
	 * @param e MotionEvent
	 * @return boolean
	 */
	public boolean getTouchEvent(MotionEvent e) {
		
		if (!isZoomMode()) {
			return false;
		}
		
		int action = e.getAction() & MotionEvent.ACTION_MASK;
		
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mBackupMatrix.set(mMatrix);
			mOffsetPoint.set(e.getX(), e.getY());
			mMode = ThumbnailMode.DRAG;
			break;
		case MotionEvent.ACTION_POINTER_DOWN:
			mDistance = getSpacing(e);
			if (mDistance > ZOOMING_DISTANCE) {
				mBackupMatrix.set(mMatrix);
				getMidPoint(mMiddlePoint, e);
				mMode = ThumbnailMode.ZOOM;
			}
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_POINTER_UP:
			mMode = ThumbnailMode.NONE;
			break;
		case MotionEvent.ACTION_MOVE:
//			if (mMode == ThumbnailMode.NONE) {
//				if (e.getPointerCount() > 1) {
//					mMode = ThumbnailMode.ZOOM;
//				} else {
//					mMode = ThumbnailMode.DRAG;
//				}
//				mBackupMatrix.set(mMatrix);
//				mOffsetPoint.set(e.getX(), e.getY());
//			}
			if (mMode == ThumbnailMode.DRAG) {
				mMatrix.set(mBackupMatrix);
				mMatrix.postTranslate(e.getX() - mOffsetPoint.x, e.getY() - mOffsetPoint.y);
			}
			else if (mMode == ThumbnailMode.ZOOM) {
				float newDist = getSpacing(e);
				if (newDist > ZOOMING_DISTANCE) {
					mMatrix.set(mBackupMatrix);
					//float scale = getMaxScale(newDist, mDistance, e);
					float scale = newDist / mDistance;
					mMatrix.postScale(scale, scale, mMiddlePoint.x, mMiddlePoint.y);
				}
			}
			break;
		default:
			break;
		}
		
		setBoundOfMatrix();
		setImageMatrix(mMatrix);
		return true;
	}
	
	/**
	 * 확대한 matrix가 이동 중 화면 영역 밖으로 이동하지 못하도록 구현
	 */
	public void setBoundOfMatrix() {
		float[] values = new float[9];
		mMatrix.getValues(values);

		int width = this.getWidth();
        int height = this.getHeight();
        float maxScale = mDefalutScale * MAXZOOM;
		
		if (values[0] > maxScale || values[4] > maxScale) {
			values[0] = maxScale;
			values[4] = maxScale;
			values[2] = getMatrixValue(mBackupMatrix2, Matrix.MTRANS_X);
			values[5] = getMatrixValue(mBackupMatrix2, Matrix.MTRANS_Y);
		} else if (values[0] < mDefalutScale || values[4] < mDefalutScale) {
			values[0] = mDefalutScale;
			values[4] = mDefalutScale;
			values[2] = getMatrixValue(mBackupMatrix2, Matrix.MTRANS_X);
			values[5] = getMatrixValue(mBackupMatrix2, Matrix.MTRANS_Y);
			if (mDragReachTheWallListener != null) {
				mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_ALL);
				getParent().requestDisallowInterceptTouchEvent(true);
			}
		}
		mOffSetScale = values[0];
        
        int scaleWidth = (int) (mImageWidth * values[0]);
        int scaleHeight = (int) (mImageHeight * values[4]);
        
        if (scaleWidth < width) {
//			if (values[2] < 0) {
//				values[2] = (width - scaleWidth)/2;
//				if (mDragReachTheWallListener != null) {
//					mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_RIGHT);
//					getParent().requestDisallowInterceptTouchEvent(true);
//				}
//			}
//			if (values[2] > mEmptyWidth) {
//				values[2] = (width - scaleWidth)/2;
//				if (mDragReachTheWallListener != null) {
//					mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_LEFT);
//					getParent().requestDisallowInterceptTouchEvent(true);
//				}
//			}
        	values[2] = (width - scaleWidth)/2;
			if (mDragReachTheWallListener != null) {
				mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_ALL);
				getParent().requestDisallowInterceptTouchEvent(true);
			}
		} else {
			if (values[2] < width - scaleWidth) {
				values[2] = width - scaleWidth;
				if (mDragReachTheWallListener != null) {
					mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_LEFT);
					getParent().requestDisallowInterceptTouchEvent(true);
				}
			}
			if (values[2] > 0) {
				values[2] = 0;
				if (mDragReachTheWallListener != null) {
					mDragReachTheWallListener.availableSlide(PhotoViewerActivity.SLIDE_RIGHT);
					getParent().requestDisallowInterceptTouchEvent(true);
				}
			}
		}
        
		if (scaleHeight < height) {
			values[5] = (height - scaleHeight)/2;
//			if (values[5] < 0) {
//				values[5] = (height - scaleHeight)/2;
//			}
//			if (values[5] > height - scaleHeight) {
//				values[5] = (height - scaleHeight)/2;
//			}
		} else {
			if (values[5] < height - scaleHeight) {
				values[5] = height - scaleHeight;
			}
			if (values[5] > 0) {
				values[5] = 0;
			}
		}
		mMatrix.setValues(values);
		mBackupMatrix2.set(mMatrix);
	}

	private float[] mMatrixValues = new float[9];
	
	/**
	 * matrix의 현재 행렬 값에서 특정 위치의 값을 가져온다.
	 * @param matrix
	 * @param number
	 * @return float
	 */
	private float getMatrixValue(Matrix matrix, int number) {
		matrix.getValues(mMatrixValues);
		return mMatrixValues[number];
	}
	
	/**
	 * 핀치 줌 시에 두 손가락의 거리를 가져온다.
	 * @param event
	 * @return float
	 */
	private float getSpacing(MotionEvent event) {
		float x = event.getX(0) - event.getX(1);
		float y = event.getY(0) - event.getY(1);
		return FloatMath.sqrt(x * x + y * y);
	}

	/**
	 * 두 손가락 사이의 가운데 점을 point에 set한다.
	 * @param point
	 * @param event
	 */
	private void getMidPoint(PointF point, MotionEvent event) {
		float x = event.getX(0) + event.getX(1);
		float y = event.getY(0) + event.getY(1);
		point.set(x / 2, y / 2);
	}

	/**
	 * 현재 Zoom상태인지 확인할 수 있다.
	 * @return boolean
	 */
	public boolean isZoomMode() {
		return mZoomMode;
	}
	
	/**
	 * 현재 MaxTapZoom상태인지 확인할 수 있다.
	 * @return boolean
	 */
	public boolean isMaxTapZoom() {
		if (mOffSetScale >= mDefalutScale * DOUBLETAPZOOM_PHASE_2) {
			return true;
		}
		return false;
	}

	/**
	 * Zoom모드를 설정하도록 한다.
	 * @param enable true-zoom진입, false-zoom빠져나오기
	 * @param maxZoom zoom모드 min_zoom(-1) 최소, normal(0) 핀치줌, max_zoom(1) 최대줌, double_tap(2) 더블탭줌
	 * @param point double_tap이라면 tap한 위치정보를 전달해준다. 아니라면 (0,0)
	 */
	public void setZoomMode(boolean enable, int maxZoom, Point point) {
		// TODO Auto-generated method stub
		if (point == null) {
			return;
		}
		
		mZoomMode = enable;
		
		float values[] = new float[9];
		mMatrix.getValues(values);
		getDisplayRect(mMatrix);
		
		switch (maxZoom) {
		case MIN_ZOOM:
			mOffSetScale = mDefalutScale;
			AnimationZoomRunnable animationZoomOutRunnable = new AnimationZoomRunnable(mOffSetScale, new Point(this.getWidth()/2, this.getHeight()/2), 500, true);
			animationZoomOutRunnable.start();
			this.post(animationZoomOutRunnable);
			mMode = ThumbnailMode.NONE;
			break;
		case NORMAL:
			break;
		case DOUBLE_TAP:
			if (!mImageRect.contains(point.x, point.y)) {
				return;
			}
			if (mOffSetScale < mDefalutScale * DOUBLETAPZOOM_PHASE_1) {
				mOffSetScale = mDefalutScale * DOUBLETAPZOOM_PHASE_1;
			} else if (mOffSetScale < mDefalutScale * DOUBLETAPZOOM_PHASE_2) {
				mOffSetScale = mDefalutScale * DOUBLETAPZOOM_PHASE_2;
			}
			AnimationZoomRunnable animationZoomInRunnable = new AnimationZoomRunnable(mOffSetScale, point, 300, false);
			animationZoomInRunnable.start();
			this.post(animationZoomInRunnable);
			mMode = ThumbnailMode.DRAG;
			break;
		case MAX_ZOOM:
			break;
		}
	}
	
	/**
	 * Zoom시에 Animation을 그려준다.
	 * @author Jang
	 *
	 */
	private class AnimationZoomRunnable implements Runnable {

		private long mStartTime = 0;
		private int mDuration = 500;
		private float mCurrentScale = 0;
		private float mTargetScale = 0;
		private float mPositionX = 0;
		private float mPositionY = 0;
		private float doubleTapImagePoint[] = new float[2];
		private Matrix inverseMatrix = new Matrix();
		private boolean isZoomOut = false;
		
		public AnimationZoomRunnable(float targetScale, Point point, int duration, boolean iszoomout) {
			mTargetScale = targetScale;
			mPositionX = point.x;
			mPositionY = point.y;
			mDuration = duration;
			this.isZoomOut = iszoomout;
		}
		
		public void start() {
			Drawable drawable = ThumbnailView.this.getDrawable();
			if (drawable == null) {
				return;
			}
			mCurrentScale = getCurrentScale();
			if (mStartTime == 0) {
				mStartTime = System.currentTimeMillis();
			}
			mMatrix.invert(inverseMatrix);
			doubleTapImagePoint[0] = mPositionX;
	        doubleTapImagePoint[1] = mPositionY;
	        inverseMatrix.mapPoints(doubleTapImagePoint);
	        if (isZoomOut) {
	        	RectF imageBounds = new RectF(drawable.getBounds());
	        	mMatrix.reset();
	        	mMatrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
	        	mMatrix.postScale(mTargetScale, mTargetScale);
	        	mMatrix.mapRect(imageBounds);
	        	
	        	mPositionX = ((ThumbnailView.this.getWidth() - imageBounds.width()) / 2.0f) - imageBounds.left;
	        	mPositionY = ((ThumbnailView.this.getHeight() - imageBounds.height()) / 2.0f) - imageBounds.top;
	        }
		}
		
		@Override
		public void run() {
			// TODO Auto-generated method stub
			float time = (float) (System.currentTimeMillis() - mStartTime) / mDuration;
			time = time > 1.0f ? 1.0f : time;
			
			float interpolatedRatio = interpolator.getInterpolation(time);
			float tempScale = mCurrentScale + interpolatedRatio * (mTargetScale - mCurrentScale);

			mMatrix.reset();
            // translate initialPoint to 0,0 before applying zoom
			mMatrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
			mMatrix.postScale(tempScale, tempScale);
			mMatrix.postTranslate(mPositionX, mPositionY);
			
			setBoundOfMatrix();
			mBackupMatrix2.set(mMatrix);
            setImageMatrix(mMatrix);
			
			if (time < 1.0f) {
				ThumbnailView.this.post(this);
			}
		}
	}
	
	public final RectF getDisplayRect() {
		setBoundOfMatrix();
	    return getDisplayRect(getDisplayMatrix());
	}
	
	private RectF getDisplayRect(Matrix paramMatrix) {
	    Drawable localDrawable = getDrawable();
	    if (localDrawable != null) {
	    	this.mImageRect.set(0.0F, 0.0F, localDrawable.getIntrinsicWidth(), localDrawable.getIntrinsicHeight());
	    	paramMatrix.mapRect(this.mImageRect);
	    	return this.mImageRect;
	    }
	    return null;
	}
	
	protected Matrix getDisplayMatrix() {
		this.mMatrix.set(this.mBackupMatrix2);
	    return this.mMatrix;
	}
	
	final Interpolator interpolator = new AccelerateDecelerateInterpolator();
	
	/**
	 * RecyclingBitmapDrawable에 있는 Bitmap을 자동으로 set or recycle하기 위한 함수이다.<br>
	 * 변경된 Drawable은 ImageFetcher에서 로딩하거나 recycle한다. 
	 * @param drawable
	 * @param isDisplayed
	 */
	private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable instanceof RecyclingBitmapDrawable) {
            // The drawable is a CountingBitmapDrawable, so notify it
            ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
        } else if (drawable instanceof LayerDrawable) {
            // The drawable is a LayerDrawable, so recurse on each layer
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
            }
        } else if (!isDisplayed && drawable instanceof BitmapDrawable) {
        	Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        	if (bitmap != null && !bitmap.isRecycled())
        		bitmap.recycle();
        }
    }

	/**
	 * 스크롤 시 다음 화면으로 이동하는 것을 감지하기 위한 Listener이다.
	 * @param mDragReachTheWallListener
	 */
	public void setDragReachTheWallListener(DragReachTheWallListener mDragReachTheWallListener) {
		this.mDragReachTheWallListener = mDragReachTheWallListener;
	}
	
	/**
	 * 현재 Image의 Scale값을 가져온다.
	 * @return float
	 */
	public float getCurrentScale() {
		float scale = 1f;
		
		scale = getMatrixValue(mMatrix, Matrix.MSCALE_X);
		
		return scale;
	}

	/**
	 * 이미지 초기화 시의 defaultscale값을 가져온다.
	 * @return float
	 */
	public float getDefaultScale() {
		// TODO Auto-generated method stub
		return mDefalutScale;
	}
	
	public RectF getCurrentRect() {
		return mImageRect;
	}
	
	public void setMatrix() {
		setImageMatrix(mMatrix);
	}

}
